package ph.com.gs3.formalistics.service.managers;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import ph.com.gs3.formalistics.global.interfaces.CallbackCommand;
import ph.com.gs3.formalistics.global.utilities.DependencyChecker;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.global.utilities.logging.FLLogger.LogType;
import ph.com.gs3.formalistics.model.communicators.AuthenticationCommunicatorModule;
import ph.com.gs3.formalistics.model.communicators.HttpCommunicator;
import ph.com.gs3.formalistics.model.communicators.HttpCommunicatorModule.CommunicatorException;
import ph.com.gs3.formalistics.model.communicators.HttpCommunicatorModule.ResultCode;
import ph.com.gs3.formalistics.model.dao.UserAccountsDAO;
import ph.com.gs3.formalistics.model.dao.UserAccountsDAO.IncompleteUserValuesException;
import ph.com.gs3.formalistics.model.dao.UserAccountsDAO.PasswordExclusion;
import ph.com.gs3.formalistics.model.dao.UserAccountsDAO.UserNotFoundException;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse.APIInvalidResponseException;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse.APIStatus;
import ph.com.gs3.formalistics.model.valueobjects.business.User;
import ph.com.gs3.formalistics.model.valueobjects.business.User.UserParseException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;

/**
 * 
 * Manages connectivity and session state of the application. This manager listens to
 * connectivity changes and checks for server availability every time a connection is
 * available. This serves as the Facade for session related functionalities.
 * 
 * @author Ervinne Sodusta
 */
public class SessionManager {

	public static final String TAG = SessionManager.class.getSimpleName();

	// =====================================================================
	// Constants & Enumerations

	public static class LoginResult implements Serializable {
		private static final long serialVersionUID = -3229689739147340919L;

		private ResultCode loginResultCode;
		private String message;
		private User activeUser;

		public ResultCode getLoginResultCode() {
			return loginResultCode;
		}

		public void setLoginResultCode(ResultCode loginResultCode) {
			this.loginResultCode = loginResultCode;
		}

		public String getMessage() {
			return message;
		}

		public void setMessage(String message) {
			this.message = message;
		}

		public User getActiveUser() {
			return activeUser;
		}

		public void setActiveUser(User activeUser) {
			this.activeUser = activeUser;
		}

	}

	public static class CheckConnectionResult {

		public int connectionStatus;
		public String errorMessage;

	}

	public enum LoginFields {
		SERVER, EMAIL, PASSWORD
	}

	public static enum ConnectivityType {
		NONE, WIFI, MOBILE_DATA
	}

	public static enum Mode {
		OFFLINE, ONLINE
	}

	// =====================================================================
	// Server errors/exceptions
	public static final String SERVER_ERROR_USER_NOT_FOUND = "User Not Found";

	// =====================================================================
	// Fields

	protected boolean isCurrentlyCheckingServerConnection;
	protected boolean isAuthenticated;
	protected Mode mode;
	protected ConnectivityType connectivityType;

	private boolean isCurrentlyNotifyingListeners;
	private List<SessionManagerEventListener> listenerAddQueue;
	private List<SessionManagerEventListener> listenerRemoveQueue;

	// =====================================================================
	// Manager Fields

	protected BroadcastReceiver broadcastReceiver;
	protected List<SessionManagerEventListener> listeners;

	// =====================================================================
	// Dependencies

	protected Context applicationContext;
	protected UserAccountsDAO userAccountsDAO;
	protected AuthenticationCommunicatorModule authCommModule;

	/**
	 * Default instance is created on the application, this is an improvised singleton
	 * where the instance is generated once and its dependencies are injected in the
	 * application class.
	 */
	protected static SessionManager applicationInstance;

	/**
	 * Gives the application instance created in the application class.
	 * 
	 * @return an improvised singleton instance of FLSessionManager.
	 */
	public static SessionManager getApplicationInstance() {
		return applicationInstance;
	}

	/**
	 * Creates a new instance of FLSessionManager
	 * 
	 * @param applicationContext
	 *            dependency, the application context.
	 * @param userAccountsDAO
	 *            dependency, a new data access object for user accounts.
	 * @param authCommModule
	 *            dependency, a new authentication communicator module.
	 */
	public SessionManager(Context applicationContext, UserAccountsDAO userAccountsDAO,
	        AuthenticationCommunicatorModule authCommModule) {

		this.applicationContext = applicationContext;
		this.userAccountsDAO = userAccountsDAO;
		this.authCommModule = authCommModule;

		listeners = new ArrayList<SessionManagerEventListener>();
		listenerAddQueue = new ArrayList<SessionManagerEventListener>();
		listenerRemoveQueue = new ArrayList<SessionManagerEventListener>();

		isAuthenticated = false;
		isCurrentlyCheckingServerConnection = false;

		isCurrentlyNotifyingListeners = false;

	}

	/**
	 * Creates the (improvised singleton) application instance
	 * 
	 * @param applicationContext
	 * @param userAccountsDAO
	 * @param authCommModule
	 * @return
	 */
	public static SessionManager createApplicationInstance(Context applicationContext,
	        UserAccountsDAO userAccountsDAO, AuthenticationCommunicatorModule authCommModule) {

		if (applicationInstance == null) {
			applicationInstance = new SessionManager(applicationContext, userAccountsDAO,
			        authCommModule);
		} else {
			throw new RuntimeException("A default instance of FLSessionManager is already created");
		}

		return applicationInstance;
	}

	// =====================================================================
	// Functional Methods

	public void startListeningToNetworkChanges() {
		initializeBroadcastReceiver();

		IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
		applicationContext.registerReceiver(broadcastReceiver, intentFilter);
	}

	/**
	 * Tries to log the user in to the specified server inside the
	 * AuthenticationCommunicatorModule object passed to this method. If the login is
	 * successful, the login data will be saved.
	 * 
	 * @param authCommModule
	 * @param email
	 * @param password
	 */
	public LoginResult login(String server, String email, String password) {

		// reset the authentication status, this will be updated again if the login was
		// successful
		isAuthenticated = false;

		LoginResult loginResult = new LoginResult();

		APIResponse serverResponse;
		try {
			// throws CommunicatorException and/or APIResponseException
			authCommModule.setServer(server);
			serverResponse = authCommModule.login(email, password);
			// FLLogger.d(TAG, "Server response on login: " +
			// serverResponse.getResults());
		} catch (CommunicatorException e) {
			FLLogger.log(TAG + ", " + AuthenticationCommunicatorModule.TAG, LogType.ERROR,
			        e.getMessage());

			loginResult.message = "Connection Failed";
			loginResult.loginResultCode = ResultCode.CONNECTION_FAILED;
			return loginResult;
		} catch (APIInvalidResponseException e) {
			FLLogger.log(TAG + ", " + AuthenticationCommunicatorModule.TAG, LogType.ERROR,
			        e.getMessage());

			loginResult.message = e.getMessage();
			loginResult.loginResultCode = ResultCode.INVALID_SERVER_RESPONSE;
			return loginResult;
		}

		if (serverResponse.getStatus() == APIStatus.SUCCESS) {

			try {
				String rawResults = serverResponse.getResults();
				// throws JSONException
				JSONObject rawResultsJSON = new JSONObject(rawResults);
				User user = User.createFromJSON(rawResultsJSON, authCommModule.getServer());

				user.setPassword(password);	// TODO: add encryption here

				userAccountsDAO.open();
				User registeredUser = userAccountsDAO.saveOrUpdateUser(user);
				userAccountsDAO.activateUser(registeredUser.getId());
				userAccountsDAO.close();

				loginResult.message = "Login Success";
				loginResult.loginResultCode = ResultCode.SUCCESS;
				loginResult.activeUser = registeredUser;

				isAuthenticated = true;

				// automatically set the mode to online if there are any successful server
				// interaction
				mode = Mode.ONLINE;

				FLLogger.d(TAG, "Successfully logged in");

				return loginResult;
			} catch (UserParseException e) {
				// TODO: throw exceptions later
				FLLogger.log(TAG, LogType.ERROR, e.getMessage());

				loginResult.message = e.getMessage();
				loginResult.loginResultCode = ResultCode.APPLICATION_ERROR;
				return loginResult;
			} catch (IncompleteUserValuesException e) {
				// TODO: throw exceptions later
				FLLogger.log(TAG, LogType.ERROR, e.getMessage());

				loginResult.message = e.getMessage();
				loginResult.loginResultCode = ResultCode.APPLICATION_ERROR;
				return loginResult;
			} catch (UserNotFoundException e) {
				// TODO: throw exceptions later
				FLLogger.log(TAG, LogType.ERROR, e.getMessage());

				loginResult.message = "Unable to register user on your device";
				loginResult.loginResultCode = ResultCode.APPLICATION_ERROR;
				return loginResult;
			} catch (JSONException e) {
				// TODO: throw exceptions later
				FLLogger.log(TAG, LogType.ERROR, e.getMessage());

				loginResult.message = "Server gave an invalid response, the results was not a valid JSON object.";
				loginResult.loginResultCode = ResultCode.INVALID_SERVER_RESPONSE;
				return loginResult;
			}
		} else {
			loginResult.message = serverResponse.getErrorMessage();
			loginResult.loginResultCode = ResultCode.LOGIN_FAILED;

			FLLogger.d(TAG, "Login failed: " + serverResponse.getErrorMessage());

			return loginResult;
		}

	}

	public User getCurrentlyLoggedInUser(String server) throws CommunicatorException,
	        APIInvalidResponseException, UserParseException, UserNotSavedOnDeviceException {

		checkDependencies();

		APIResponse response = authCommModule.getCurrentlyLoggedInUser();

		if (response.getStatus() == APIStatus.SUCCESS) {
			// throws UserParseException
			String rawResults = response.getResults();
			// throws JSONException
			JSONObject rawResultsJSON;
			User user;
			try {
				rawResultsJSON = new JSONObject(rawResults);
				user = User.createFromJSON(rawResultsJSON, authCommModule.getServer());
			} catch (JSONException e) {
				throw new APIInvalidResponseException(e);
			}

			user.setAccountServer(authCommModule.getServer());

			int webId = user.getWebId();
			// String server = user.getAccountServer();

			// get the user from the database
			userAccountsDAO.open();
			User registeredUser = userAccountsDAO.getUserWithWebIdAndServer(webId, server);

			if (registeredUser == null) {
				throw new UserNotSavedOnDeviceException();
			} else {
				// update the user information except password
				userAccountsDAO.updateUser(user, webId, server, PasswordExclusion.EXCLUDE_PASSWORD);
			}

			userAccountsDAO.close();

			// automatically set the mode to online if there are any successful server
			// interaction
			mode = Mode.ONLINE;

		} else if (SERVER_ERROR_USER_NOT_FOUND.equals(response.getError())) {
			return null;
		} else {
			// TODO: create a specific exception for this
			throw new CommunicatorException(response.getErrorMessage());
		}

		return null;

	}

	protected void initializeBroadcastReceiver() {

		broadcastReceiver = new BroadcastReceiver() {

			@Override
			public void onReceive(Context context, Intent intent) {
				ConnectivityType checkedConnectivityType = checkConnectivityType();
				connectivityType = checkedConnectivityType;

				switch (connectivityType) {
					case NONE: {
						mode = Mode.OFFLINE;
						notifyListenersModeChanged(mode);

						FLLogger.d(TAG,
						        "Mode changed: Offline mode, no wifi or mobile connection available");
					}
						break;
					case WIFI:
					case MOBILE_DATA: {
						checkConnectionToServer(new CallbackCommand<CheckConnectionResult>() {

							@Override
							public void execute(CheckConnectionResult result) {
								String modeChangeDebugMessage = "Mode changed: ";

								if (result.connectionStatus == HttpCommunicator.STATUS_CONNECTED) {
									mode = Mode.ONLINE;
									modeChangeDebugMessage += "Online mode";
								} else if (result.connectionStatus == HttpCommunicator.STATUS_ERROR_ON_CONNECT) {
									mode = Mode.OFFLINE;
									modeChangeDebugMessage += "Offline mode, "
									        + result.errorMessage;

									notifyListenersSessionLost();
								} else {
									mode = Mode.OFFLINE;
									modeChangeDebugMessage += "Offline mode, unable to connect to server";
								}

								FLLogger.d(TAG, modeChangeDebugMessage);

								notifyListenersModeChanged(mode);
							}

						});
					}
						break;
				}

			}

		};
	}

	/**
	 * Checks the type of connection the mobile phone has. The priority of checking is
	 * WIFI -> Mobile -> None, this means that if this method finds that WIFI is
	 * available, it wont bother checking if mobile data is available.
	 * 
	 * @return current connection type
	 */
	public ConnectivityType checkConnectivityType() {

		ConnectivityManager connMan = (ConnectivityManager) applicationContext
		        .getSystemService(Context.CONNECTIVITY_SERVICE);

		NetworkInfo wifiNetworkInfo = connMan.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

		if (wifiNetworkInfo != null) {
			if (wifiNetworkInfo.isConnected()) {
				return ConnectivityType.WIFI;
			}
		}

		NetworkInfo mobileNetworkInfo = connMan.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);

		if (mobileNetworkInfo != null) {
			if (mobileNetworkInfo.isConnected()) {
				return ConnectivityType.MOBILE_DATA;
			}
		}

		return ConnectivityType.NONE;

	}

	public synchronized void checkConnectionToServer(
	        final CallbackCommand<CheckConnectionResult> callback) {

		AsyncTask<Void, Void, CheckConnectionResult> checkConnectionTask = new AsyncTask<Void, Void, CheckConnectionResult>() {

			@Override
			protected CheckConnectionResult doInBackground(Void... params) {
				User currentlyActiveUser = getLastActiveUserFromDatabase();

				CheckConnectionResult result = new CheckConnectionResult();

				if (currentlyActiveUser != null) {
					String server = currentlyActiveUser.getAccountServer();

					FLLogger.d(TAG, "Checking connection to: " + server);
					int connectionStatus = authCommModule.getCommunicator().testConnection(server);

					result.connectionStatus = connectionStatus;
					result.errorMessage = null;

					if (connectionStatus == HttpCommunicator.STATUS_CONNECTED) {
						FLLogger.d(TAG, "Connected");
					} else {
						FLLogger.d(TAG, "Disconnected");
					}

				} else {
					result.connectionStatus = HttpCommunicator.STATUS_ERROR_ON_CONNECT;
					result.errorMessage = "No active user yet";
				}

				return result;

			}

			@Override
			protected void onPostExecute(CheckConnectionResult result) {
				super.onPostExecute(result);

				isCurrentlyCheckingServerConnection = false;
				if (callback != null) {
					callback.execute(result);
				}
			}

		};
		checkConnectionTask.execute();
		isCurrentlyCheckingServerConnection = true;
		FLLogger.d(TAG, "Now checking server connection");

	}

	public void logout() {

		// remove active user
		userAccountsDAO.open();
		userAccountsDAO.deactivateAllUsers();
		userAccountsDAO.close();

	}

	// =====================================================================
	// Utility Methods

	protected void checkDependencies() {

		String[] dependencyClasses = { "UserAccountsDAO", "AuthenticationCommunicatorModule" };
		Object[] dependencies = { userAccountsDAO, authCommModule };
		DependencyChecker.checkDependencies(dependencyClasses, dependencies);

	}

	// ============================================================================
	// Observation Methods

	protected synchronized void notifyListenersModeChanged(Mode mode) {

		isCurrentlyNotifyingListeners = true;

		for (SessionManagerEventListener listener : listeners) {
			listener.onModeChanged(mode);
		}

		isCurrentlyNotifyingListeners = false;

	}

	/**
	 * Unsupported
	 */
	protected synchronized void notifyListenersSessionLost() {

		isCurrentlyNotifyingListeners = true;

		FLLogger.d(TAG, "listener count: " + listeners.size());

		for (SessionManagerEventListener listener : listeners) {
			listener.onSessionLost();
		}

		isCurrentlyNotifyingListeners = false;

	}

	public synchronized void addListener(SessionManagerEventListener listener) {
		if (isCurrentlyNotifyingListeners) {
			listenerAddQueue.add(listener);
		} else {
			listeners.add(listener);
		}
	}

	public synchronized void removeListener(SessionManagerEventListener listener) {
		if (isCurrentlyNotifyingListeners) {
			listenerRemoveQueue.add(listener);
		} else {
			listeners.remove(listener);
		}
	}

	protected synchronized void applyQueuedListeners() {

		if (listenerAddQueue.size() > 0) {
			listeners.addAll(listenerAddQueue);
			listenerAddQueue.clear();
		}

		if (listenerRemoveQueue.size() > 0) {
			listeners.removeAll(listenerRemoveQueue);
			listenerRemoveQueue.clear();
		}

	}

	// ========================================================================
	// Interfaces

	public static interface SessionManagerEventListener {
		public void onModeChanged(Mode mode);

		public void onSessionLost();
	}

	// ============================================================================
	// Exceptions

	public static final class LoginException extends Exception {

		private static final long serialVersionUID = -1799369965596786842L;

		private LoginFields[] affectedFields;

		public LoginException(String message, LoginFields[] affectedFields) {
			super(message);
		}

		public LoginFields[] getAffectedFields() {
			return this.affectedFields;
		}

	}

	public static final class UserNotSavedOnDeviceException extends Exception {

		private static final long serialVersionUID = 3901545589995827721L;

		public UserNotSavedOnDeviceException() {
			super(
			        "The currently logged in user is not yet registered on this device, please login again");
		}

	}

	// ========================================================================
	// Getter & Setter Methods

	/**
	 * Searches the database for the last active user, if no such user is found, this
	 * method returns null.
	 * 
	 * @return
	 */
	public User getLastActiveUserFromDatabase() {

		userAccountsDAO.open();
		User lastActiveUser = userAccountsDAO.getActiveUserAccount();
		userAccountsDAO.close();

		return lastActiveUser;

	}

	public Mode getMode() {
		return mode;
	}

	public ConnectivityType getConnecctivityType() {
		return connectivityType;
	}

	public boolean isAuthenticated() {
		return isAuthenticated;
	}

	public synchronized boolean isCurrentlyCheckingServerConnection() {
		return isCurrentlyCheckingServerConnection;
	}
}
